<?php
/**
 * Created by PhpStorm.
 * User: Admin
 * Date: 2017-10-26
 * Time: 下午 09:02
 */
namespace VRS\Tunnel;
date_default_timezone_set('PRC');
error_reporting(E_ALL ^ E_NOTICE);
set_time_limit(0);

define('SECURE_KEY', /*#[SECURE_KEY]*/'asdasd'/*[SECURE_KEY]#*/);
define('RELEASE_DIR', /*#[RELEASE_DIR]*/'./'/*[RELEASE_DIR]#*/);
$GLOBALS['_config'] = [
    /*#[_CONFIG]*/ 
    
    /*[_CONFIG]#*/
];

$GLOBALS['_allow_commands'] = [
    ['cmd' => 'ls']
    /*#[_ALLOW_COMMANDS]*/ /*[_ALLOW_COMMANDS]#*/
];

/**
 * @param null $key
 * @return array|string
 */
function get($key=null, $default=null) {
    global $_g_arguments, $_g_arguments_raw;

    if(!empty($_g_arguments)) {
        if($key === null) {
            return (array)$_g_arguments;
        } else {
            return $_g_arguments[$key];
        }
    }

    $args = $_SERVER['argv'];
    unset($args[0]);
    $pointer = null;

    foreach((array)$args as $row) {
        if(substr($row, 0, 2) == '--') {
            $_g_arguments_raw []= $row;
            $_g_arguments [substr($row, 2)] = true;
        } else if(substr($row, 0, 1) == '-') {
            $_g_arguments_raw []= $row;
            $pointer = &$_g_arguments [substr($row, 1)];
        } else {
            $pointer = $row;
            unset($pointer);
        }
    }

    $_g_arguments = array_merge((array)$_g_arguments, $_REQUEST);
    
    if($key === null) {
        return (array)$_g_arguments;
    } else {
        return empty($_g_arguments[$key]) ? $default : $_g_arguments[$key];
    }
}

function set($key, $value) {
    global $_g_arguments;
    $_g_arguments[$key] = $value;
}

function check()
{
    global $_g_arguments_raw;
    get();

    $allow_args_list []= '--help';
    foreach((array)$_g_arguments_raw as $value) {
        if(!in_array($value, $allow_args_list)) {
            exitln("Unsupported argument '$value'");
        }
    }
}

function json($ob)
{
    header('content-type:application/json');
    echo json_encode($ob);
    die;
}

function fail_json($message, $data=null, $code=1)
{
    if($code == 0) {
        $code = 1;
    }
    
    json(['code' => $code, 'message' => $message, 'data' => $data, 'ok' => false, 'fail' => true]);
}

function ok_json($message, $data=null)  {
    json(['code' => 0, 'message' => $message, 'data' => $data, 'ok' => true, 'fail' => false]);
}

function fail_output($message, $data=null, $code=1)
{
    if(isset($_SERVER['shell'])) {
        echo $message;
        die;
    } else {
        fail_json($message, $data, $code);
    }
}

function outputln($message) 
{
    if(isset($_SERVER['shell'])) {
        echo $message . "\n";
    }
}

function exit_output($data=null, $message=null)
{
    if(isset($_SERVER['shell'])) {
        outputln($message);
        die;
    } else {
        ok_json($message, $data);
    }
}

function timecost_begin($key=null) {
    global $_g_timecost, $_g_timecost_begin;

    if(empty($key)) {
        $_g_timecost_begin = microtime(true);
    } else {
        $_g_timecost [$key] = microtime(true);
    }
}

function timecost($key=null) {
    global $_g_timecost, $_g_timecost_begin;

    if(empty($key)) {
        if(!is_float($_g_timecost_begin)) {
            timecost_begin($key);
            return 0;
        }

        return round(microtime(true) - $_g_timecost_begin, 4);
    } else {
        if(!is_float($_g_timecost [$key])) {
            timecost_begin($key);
            return 0;
        }

        return round(microtime(true) - $_g_timecost[$key], 4);
    }
}

function replace_php_flags($content, $key, $value, $replaceAll=false)
{
    $begin = "/*#[$key]*/";
    $end = "/*[$key]#*/";
    $beginLength = strlen($begin);
    $endLength = strlen($end);
    $valLength = strlen($value);
    $pos = 0;
    
    while (($pos = strpos($content, $begin, $pos)) !== false) {
        $endpos = strpos($content, $end, $pos + $beginLength);
        
        if($endpos === false) {
            return $content;
        }
        
        $content = substr($content, 0, $pos+$beginLength) . $value . substr($content, $endpos);
        $pos = $pos + $beginLength + $valLength + $endLength;
        
        if(!$replaceAll) {
            return $content;
        }
    }
    
    return $content;
}

function force_utf8($str) {
    $encode = mb_detect_encoding($str, array("ASCII", 'UTF-8', "GB2312", "GBK",'BIG5'));
    
    if(strtolower($encode) != 'utf-8') {
        return iconv($encode,"UTF-8//IGNORE", $str); 
    }
    
    return $str;
}

/**
 * @param $cmd
 * @return CmdResult
 */
function run_cmd($cmd) {
    $descriptorspec = array(
        0 => array("pipe", "r"),    // stdin
        1 => array("pipe", "w"),    // stdout
        2 => array("pipe", "w")     // stderr
    );

    $proc = proc_open($cmd, $descriptorspec, $pipes, null, null);

    // $proc为false，表明命令执行失败
    if ($proc == false) {
        return new CmdResult($cmd);
    } else {
        $stdout = stream_get_contents($pipes[1]);
        fclose($pipes[1]);
        $stderr = stream_get_contents($pipes[2]);
        fclose($pipes[2]);
        $status = proc_close($proc);

        $stdout = force_utf8($stdout);
        $stderr = force_utf8($stderr);
        
        return new CmdResult($cmd, $status, $stdout, $stderr);
    }
}

function str_assign($str, $data)
{
    $____0 = str_replace('"', '\\"', $str);
    unset($str);
    extract($data);
    return eval("return \"$____0\";");
}

final class Application
{
    protected $_releaseCmd = [
        "git stash",
        "git checkout master",
        "git fetch --all",
        "git reset --hard origin/master",
    ];
    
    public $git;
    
    public function __construct()
    {
        set_exception_handler([$this, '__exception']);
        
        $function = ltrim(trim(get('function')), '_');
        
        if(empty($function)) {
            $this->_empty();
            die;
        }
        
        $this->_auth();
        
        $action = $function . 'Action';
        
        if(!method_exists($this, $action)) {
            fail_output("Function does not match '$function'");
        }
        
        $this->git = new Git();
        $this->$action();
    }
    
    protected function _empty()
    {
        $actions = array_filter(get_class_methods($this), function($row){
            return strtolower(substr($row, -6)) == 'action';
        });
        
        echo implode("\n" . (isset($_SERVER['shell']) ? '' : '<br>'), array_map(function($row) {return substr($row, 0, strlen($row)-6);}, $actions));
    }
    
    protected function _auth()
    {
        if(empty(SECURE_KEY)) {
            fail_output('Server unavailable');
        }
        
        $key = trim(get('key'));
        if($key != SECURE_KEY) {
            fail_output('Key Error!');
        }
    }

    public function __exception(\Exception $exception)
    {
        fail_output($exception->getMessage(), $exception->getTraceAsString(), $exception->getCode());
    }
    
    
    
    public function getVersionListAction()
    {
        $page = get('page', 1);
        $pageSize = get('pageSize');
        $versions = $this->git->fetchTagList($page, $pageSize);
        exit_output($versions);
    }
    
    public function updateKeyAction()
    {
        $newkey = get('newkey');
        $newkey = trim($newkey);

        if(empty($newkey)) {
            fail_output("Missing argument 'newkey'");
        }
        
        $newkey = "'$newkey'";
        $content = file_get_contents(__FILE__);
        $content = replace_php_flags($content, 'SECURE_KEY', $newkey);
        file_put_contents(__FILE__, $content);
        
        exit_output('ok');
    }
    public function updateReleaseDirAction()
    {
        $dir = get('dir', './');

        $newkey = "'$dir'";
        $content = file_get_contents(__FILE__);
        $content = replace_php_flags($content, 'RELEASE_DIR', $newkey);
        file_put_contents(__FILE__, $content);
        
        exit_output('ok');
    }
    
    public function releaseAction()
    {
        chdir(RELEASE_DIR);
        
        foreach ($this->_releaseCmd as $cmd) {
            $result = run_cmd($cmd);

            if(!$result->getOk()) {
                fail_output($result->getMessage(), $result->getData());
            }
        }
        
        exit_output();
    }
    
    public function getCommandsAction()
    {
        exit_output($GLOBALS['_allow_commands']);
    }
    
    public function runCmdAction()
    {
        $cmdKeys = array_filter(array_map('trim', explode(',', get('cmds'))), function($row){
            return !empty($row) || $row === '0';
        });
        
        if(empty($cmdKeys)) {
            fail_output("Missing argument 'cmds'");
        }
        
        foreach ($cmdKeys as $key) {
            $cmd = $GLOBALS['_allow_commands'][$key]['cmd'];
            $result = run_cmd($cmd);
            
            if(!$result->getOk()) {
                fail_output($result->getMessage(), $result->getData());
            }
        }

        exit_output();
    }
    
}

new Application();


/**
 * Class Object
 * @package VRS\Tunnel
 */
class Object
{
    protected $_data;

    /**
     * @param $name
     * @param $arguments
     * @return $this
     * @throws \Exception
     */
    public function __call($name, $arguments)
    {
        $operation = substr($name, 0, 3);
        $value = $arguments[0];
        $key = substr($name, 3);
        
        switch ($operation) {
            case 'get':
                return $this->_data[$key];
                break;
                
            case 'set':
                $this->_data[$key] = $value;
                return $this;
                break;
        }
        
        throw new \Exception("Method '".$name."' is not exists in class '".self::class."'");
    }
    
    public function getData($key=null)
    {
        if($key === null) {
            return $this->_data;
        }
        return $this->_data[$key];
    }
    
    public function setData($key, $val)
    {
        if(is_array($key)) {
           $this->_data = $key;     
        } else {
            $this->_data[$key] = $val;
        }
        
        return $this;
    }
}

/**
 * @method getOk
 * @method getCode
 * @method getMessage
 * @method getContent
 * @method getCmd
 * @method $this setOk($val)
 * @method $this setCode($val)
 * @method $this setMessage($val)
 * @method $this setContent($val)
 * @method $this setCmd($val)
 * Class CmdResult
 * @package VRS\Tunnel
 */
class CmdResult extends Object
{
    public function __construct($cmd, $code=1, $content=null, $messsage=null)
    {
        $this->setCode($code);
        $this->setOk($this->getCode() == 0);
        $this->setMessage($messsage);
        $this->setContent($content);
        $this->setCmd($cmd);
    }
}

class Git extends Object
{
    protected $_gitLogFormatting = [
        'id' => '%H',
        'short_id' => '%h',
        'tree_id' => '%T',
        'short_tree_id' => '%t',
        'parent_id' => '%P',
        'short_parent_id' => '%p',
        'author_name' => '%an',
        'author_email' => '%ae',
        'author_date' => '%ad',
        'author_date_unix' => '%at',
        'committer_name' => '%cn',
        'committer_email' => '%ce',
        'committer_date' => '%cd',
        'committer_date_unix' => '%ct',
        'comment' => '%s',
        'ref_names' => '%d',
        'ref_names_without_wrapping' => '%D',
        'newline' => '%n'
    ];
    
    protected $_cmds;
    protected $_workDir='.';
    
    public function __construct($dir='.')
    {
        $this->_cmds = [
            'fetch_log' => "git log {\$after} {\$before} -{\$rows} --date=unix --skip={\$pageSize} --stat --format=%n%n%n%n%nrefs:%D%nid:%H%nshort_id:%h%nname:%cn%ntime:%ct%nemail:%ce%nparent:%P%ncomment:%s",
            'fetch_tag_info' => "git show {\$tag} -s",
            'fetch_tag_list' => "git tag",
            'fetch_current' => "git symbolic-ref --short HEAD",
            'fetch_current_commit' => "git rev-parse HEAD",
        ];
        
        $this->changeWorkDir($dir);
    }
    
    public function changeWorkDir($dir)
    {
        $this->_workDir = $dir;
    }

    public function runCmd($cmdkey, $params=[])
    {
        $cmd = $this->_cmds[$cmdkey];
        $cmd = str_assign($cmd, $params);
        return run_cmd($cmd);
    }
    
    public function fetchLog($page=1, $begin=null, $end=null, $rows=30)
    {
        $afterCmdOption = $begin ? '"--after='.$begin.'"' : '';
        $beforeCmdOption = $end ? '"--before='.$end.'"' : '';
        
        $result = $this->runCmd('fetch_log', [
            'rows' => $rows,
            'pageSize' => $rows*($page-1),
            'after' => $afterCmdOption,
            'before' => $beforeCmdOption
        ]);
        
        if(!$result->getOk()) {
            throw new \Exception($result->getMessage(), $result->getCode());
        }
        
        $content = $result->getContent();
        $content = str_replace("\r\n", "\n", $content);
        $allAry = explode("\n\n\n\n\n", $content);
        $logs = [];
        
        foreach($allAry as $rowCommit) {
            $info = [];
            $rowCommit = trim($rowCommit);
            
            if(empty($rowCommit)) {
                continue;
            }
            
            list($commitInfo, $fileInfo) = explode("\n\n", $rowCommit);
            
            foreach(explode("\n", $commitInfo) as $row) {
                list($key, $value) = explode(':', $row, 2);
                $info[trim(strtolower($key))] = trim($value);
            }
            
            $fileInfo = explode("\n", trim($fileInfo));
            $fileSummaryInfo = trim(array_pop($fileInfo));
            $fileList = [];
            foreach($fileInfo as $row) {
                $row = trim($row);
                preg_match('#^([^|]+)\s\|\s([0-9]+)\s([\+\-]+)$#', $row, $rowMatchs);
                $fileList []= ['name' => $rowMatchs[1], 'count' => $rowMatchs[2], 'grap' => $rowMatchs[3]];
            }
            preg_match('#^([0-9]+)[^0-9]+?,\s*([0-9]+)[^0-9]+?,\s*([0-9]+)[^0-9]+?$#', $fileSummaryInfo, $summaryMatchs);
            $info['modify_summary'] = [
                'file_change' => $summaryMatchs[1], 
                'insertions' => $summaryMatchs[1],
                'deletions' => $summaryMatchs[2]
                ];
            $info['modify_files'] = $fileList;
            
            preg_match_all('#tag:\s*([^,]+)\s*#', $info['refs'], $matchs);
            preg_match('#tag:\s*[vV][/\\\\]([0-9]+(?:\.[0-9]+)*)\s*,?\s*#', $info['refs'], $verMatchs);

            $info['time'] = date('Y-m-d H:i:s', $info['time']);
            $info['version'] = $verMatchs[1];
            $info['tags'] = $matchs[1];
            $logs [$info['id']]= $info;
        }
        
        return $logs;
    }
    
    public function fetchCurrent()
    {
        $result = $this->runCmd('fetch_current');

        if(!$result->getOk()) {
            $result = $this->runCmd('fetch_current_commit');

            if(!$result->getOk()) {
                throw new \Exception($result->getMessage(), $result->getCode());
            }
        }
        
        return trim($result->getContent());
    }
    
    public function fetchTagList($page=1, $pageSize=20)
    {
        $page = intval($page);
        $page = $page < 1 ? 1 : $page;
        $pageSize = intval($pageSize);
        $pageSize = $pageSize < 1 ? 20 : $pageSize;
        
        $result = $this->runCmd('fetch_tag_list');
        
        if(!$result->getOk()) {
            throw new \Exception($result->getMessage(), $result->getCode());
        }
        
        $content = $result->getContent();
        $oriTags = array_filter(array_map('trim', explode("\n", $content)), function($row) {
            return !empty($row) && strtolower(substr($row, 0, 2)) == 'v/';
        });
        
        usort($oriTags, function($rowa, $rowb) {
            $versionA = substr($rowa, 2);
            $versionB = substr($rowb, 2);
            $verSegmentA = array_map(function($row) {
                return str_pad($row, '8', '0', STR_PAD_LEFT);
            }, explode('.', $versionA));
            $verSegmentB = array_map(function($row) {
                return str_pad($row, '8', '0', STR_PAD_LEFT);
            }, explode('.', $versionB));
           
            return implode('.', $verSegmentA) < implode('.', $verSegmentB);
        });
        
        $count = count($oriTags);
        $tagCount = 0;
        $tags = [];
        
        for($i=($page-1)*$pageSize; $tagCount<$pageSize && $i<$count; $i++) {
            $tagname = $oriTags[$i];
            $version = strtolower(substr($tagname,  2));
            $tag = array_merge([
                'ver' => $version,
                'tagname' => $tagname
            ], $this->fetchTagInfo($tagname));

            $tag['commits'] = $this->fetchVersionTagCommits($tag);
            $tags [$version] = $tag;
            $tagCount ++;
        }
        
        return $tags;
    }
    
    public function fetchTagInfo($tag)
    {
        $result = $this->runCmd('fetch_tag_info', ['tag' => $tag]);
        if(!$result->getOk()) {
            throw new \Exception($result->getMessage(), $result->getCode());
        }
        
        $content = $result->getContent();
        preg_match('#Date:\s*(.+?)[-+][0-9]+\s*\n\n([\s\S]+?)\n\ncommit ([0-9a-zA-Z]+)[\s\S]*Date:\s*(.+?)[-+][0-9]+#i', $content, $matchs);
        $time = strtotime(trim($matchs[1]));
        $date = date('Y-m-d H:i:s', $time);
        $first_time = strtotime(trim($matchs[4]));
        $first_date = date('Y-m-d H:i:s', $first_time);
        return ['time' => $time, 'date' => $date, 'comment' => trim($matchs[2]), 'first' => $matchs[3], 'first_time' => $first_time, 'first_date' => $first_date];
    }
    
    public function fetchVersionTagCommits($tag)
    {
        $currentVersion = $tag['ver'];
        $beginDate = $tag['first_date'];
        $commits = [];
        $page = 1;
        $count = 0;
        $max = 200;
        $found = false;
        
        while(true) {
            $logs = $this->fetchLog($page++, null, $beginDate);
            
            if(empty($logs)) {
                break;
            }
            
            foreach($logs as $log) {
                if($count >= $max) {
                    break;
                }
                
                $logVer = $log['version'];
                
                if($found && !empty($logVer) && $logVer != $currentVersion) {
                    break;
                }
                
                if($logVer == $currentVersion) {
                    $found = true;
                }
                
                if($found) {
                    $commits [$log['id']] = $log;
                    $count ++;
                }
            }
        }
        
        return $commits;
    }
}